iT邦幫忙

2023 iThome 鐵人賽

DAY 6
2
Software Development

Rust Web API 從零開始系列 第 6

Day06 - 實做訂閱API

  • 分享至 

  • xImage
  •  

我們先把訂閱API的路由加上去,目前main.rs的內容如下:

use axum::{http::StatusCode, routing::get, Router};

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/health_check", get(health_check))
        //// 在路由中加上訂閱的API
        .route("/subscriptions", post(subscribe));

    axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
        .serve(app.into_make_service())
        .await
        .unwrap();
}

pub async fn health_check() -> StatusCode {
    StatusCode::OK
}

上面的方法中還缺少subscribe的處理函數,接下來就要實做它。

Subscribe Handler

subscribe和前面health_check不一樣的地方在於,需要從HTTP Request中提取客戶端傳送的訊息,我們先實現一版最簡單的處理函數,接收請求、提取訊息,並簡單的回覆結果。

首先將serde的參考加入專案中:

cargo add serde --features derive

serde是rust中用於處裡資料序列化的crate,只要在結構前面加上巨集標注就可以簡單的讓結構支援序列化。安裝完後加上以下程式碼:

#[derive(Deserialize)]
pub struct NewSubscriber {
    /// Subscriber Email
    pub email: Option<String>,
    /// Subscriber Name
    pub name: Option<String>,
}

pub async fn subscribe(Form(data): Form<NewSubscriber>) -> StatusCode {
    if data.email.is_some() && data.name.is_some() {
        StatusCode::OK
    } else {
        StatusCode::BAD_REQUEST
    }
}

簡單說明一下,這個hander會從request中題取出訂閱者的email與名稱,如果兩者都有成功獲取得到就會回覆200,否則就會回覆400。

Extractor

我們現在把焦點放在subscribe的參數宣告部份Form(data): Form<NewSubscriber>,在Axum中,Form稱為提取器(Extractor),作用就是解析請求中的資訊,在這裡使用Form就可以從application/x-www-form-urlencoded中取得資料。
正如回傳值一樣,處理函數的參數要實做另一個traitFromRequest或是FromRequestParts,通常情況下我們也不用自己實做,Axum中已經準備好了幾個內建的提取器

  • Path: 可以從路徑中取得參數
  • Query: 可以從Query string中取得參數
  • Json: 從body中取得json格式的資料
    其他的部份請參考官方文件,不過文件中並沒有列出所有的Extractor,像是Form就不在其中。

NewSubscriber

接下來再把目光轉移到NewSubscriber這個結構體上。我在這邊使用了Option<String>這個型別去解析email與名稱,讓我們討論一下如果使用單純String的情況:

pub struct NewSubscriber {
    pub email: String,
    pub name: String,
}

如果不使用Option,這個兩個欄位就會是必填,在rust中沒有null的概念,這意謂如果使用者在發出請求時沒有給其中的值,就會在反序列化資料時解析失敗,在這個情況下Axum會自動回覆使用者400的HTTP狀態,請求也不會傳遞到handler中。
如果要讓請求能夠進到handler內部讓程式可以做處理,就必須使用Option這個Enum來讓欄位是可選的。Option是FP中的重要概念,用來表達一個值是否存在,在rust中為了處理Option提供了很多的方法,像是pattern match等技巧,在這邊只是半成品所以簡單的使用is_some()來做判斷。
是否要使用Option這個問題,就看開發上的需求而定,如果只需要簡單回覆客戶端錯誤,直接用String就可以了,如果有更複雜的業務情境可能就要加上Option了。在具有null的語言中通常不太會考慮這麼多,而Option則是把這個問題擺到檯面上,可能會比較繁瑣,但相應的好處是開發階段考慮過後就可以增進程式的穩定性。
最後稍微提一下這邊使用具有所有權的String而不是不具備所有權的str,因為在處理request的時候會需要對輸入的字串做各種加工。

FromRequest trait

最後我們來看一下FromRequesttrait

pub trait FromRequest<S, B, M = ViaRequest>: Sized {
    type Rejection: IntoResponse;

    // Required method
    fn from_request<'life0, 'async_trait>(
        req: Request<B>,
        state: &'life0 S
    ) -> Pin<Box<dyn Future<Output = Result<Self, Self::Rejection>> + Send + 'async_trait, Global>>
       where 'life0: 'async_trait,
             Self: 'async_trait;
}

這真的是一個非常複雜的trait阿,我們把它拆成幾個部份

  1. 關聯型別
    我們來看一下第二行:
    type Rejection: IntoResponse;
    
    這是一個trait的關聯型別,它同時出現在第八行的Result<Self, Self::Rejection>>中,用來指定from_request在錯誤時的回傳類型,這是rust中一種對於抽象界面描述的方式,不會具體定義trait中所使用的型別,但又能對型別做一定的限制。
  2. Sized trait
    再來看到第一行最後面的:Sized,這是一個標記,表示實做FromRequesttrait的型別需要在編譯時期就能知道大小,舉例來說u8就是固定大小的,而StringVec<T>則是會預先分配大小,在執行時期動態擴張,所以也是Sized,像是切片型別這種在執行時期才能確定要擷取哪一段就不是Sized,這個標記是為了讓編譯器能夠更好的在編譯時期處裡記憶體分配。
  3. Pin<Box<dyn Future<Output = Result<Self, Self::Rejection>> + Send + 'async_trait, Global>>
    這一行裡面有非常多資訊
    1. Future trait
      這表示一個非同步的結果,可以想像成C#中的Task,當任務結束的時候會回傳一個Result的結果。
    2. Box struct
      這是一個指標類型,由於Future的結果在編譯時期未知,所以要用一個已知大小的指針來指向未知的結果,可以理解成C#中的參考型別。
    3. Pin struct
      Pin表示這個結果被固定在特定位置,不管參數如何傳遞都可以在相同位置找到它,他是為了要在非同步任務中能夠確保資料可以被獲取,關於Pin要解決的問題請參考這篇
    4. Send trait
      這是一個標記用的trait,通常由編譯器自動判斷,在這邊表示這個Future可以安全的傳遞到不同的執行緒。Send trait是用來解決多執行緒下資料爭用的問題,通常具有不變性的資料都具有send的特徵。

小結

今天完成了訂閱API的殼,覺得在解釋FromRequest真的大大超出我現有的能力,更不用提實做它了,萬幸通常情況下我們不用自己去實做,只要使用現成的Extractor就好了,明天就繼續推進進度吧!


上一篇
Day05 - 從Server到Handler
下一篇
Day07 - 用python進行整合測試
系列文
Rust Web API 從零開始30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言